#include <SDL/SDL.h>
#include <SDL/SDL_opengl.h>
#include <GL/freeglut.h>
#include <portaudio.h>
#include <CBox.h>
#include <utils.h>
#include <font.h>
#include <console.h>
#include <fpsmgr.h>
#include <input.h>
#include <viewwin.h>
#include <profiler.h>
#include <vector>
#include <iostream>
#include <fstream>
#include <list>
#include <CParticle.h>
#include <pthread.h>
using namespace std;

console cons;
const int fnt_width = 12, fnt_height = 30;
font fnt ("../data/jag.fnt", fnt_width, fnt_height);


int CParticle::NUM_PARTICLES = 0;
CParticle* CParticle::PARTICLES = 0;
int CParticle::ALLOC_PARTICLES = 0;
int CParticle::ALLOC_AFFECTS = 0;
int CParticle::NUM_AFFECTS = 1;
vector<int> CParticle::opcodes;

// thread works on portion of CParticle::PARTICLES
struct work_t {
	int start; // start particle
	int end; // end particle
};

void* worker (void* data) {
	// thread callback
	// works on portion of CParticle::PARTICLES
	work_t* w = (work_t *) data;
	for (int i = w->start, j = w->end; i < j; ++i) CParticle::PARTICLES[i].eval ();
	pthread_exit (0);
}

class project_p {

	viewport view;

	fpsmgr fps_mgr;

	int prev_mousex, prev_mousey;

	// drawables
	GLuint axis, floor, cursor, particle;
	bool show_floor;

	// sim
	clock_t seed;
	int link2draw;
	bool auto_reset;
	unsigned int auto_reset_evals;

	// particle params
	int nparticles;
	int delta_particles;
	int naffects;

	// camera
	const static double POSX = 0, POSY = 1.25, POSZ = 5; // base pos
	double posx, posy, posz; // current pos
	double pitch, yaw, roll;
	double zx, zy, zz; // interest vector
	double xx, xy, xz; // handle vector

	// threads
	pthread_t* threads;
	work_t* work;
	int nthreads;

	// help
	vector<string> help;


	void load_help () {
		ifstream file ("../data/help.txt", ios::in);
		string s;
		while (getline(file, s)) help.push_back (s);
		cons << "Press F1 for help" << eol;
	}

	void show_help () {
		for (int i = 0, j = help.size (); i < j; ++i) cons << help[i] << eol;
	}

	bool read_input () {

		SDL_Event event;
		while ( SDL_PollEvent(&event) ) {
			switch(event.type) {
				case SDL_VIDEORESIZE:
					try {
						view = viewport ("p", event.resize.w, event.resize.h);
					} catch (const badviewport& bv) {
						cout << "sdl: viewport init failed" << endl;
						return false;
					}
					break;

				case SDL_QUIT:
					return false;
					break;
			}
		}

		read_keybd ();

		prev_mousex = mousex;
		prev_mousey = mousey;
		read_mouse ();

		return true;

	}

	bool handle_input () {

			if (read_input()) {

				// help
				if (keypressed (SDLK_F1)) show_help ();

				// quit
				if (keypressed (SDLK_ESCAPE)) return false;

				// pause/resume
				if (keypressed (SDLK_SPACE))
					if (fps_mgr.is_paused()) fps_mgr.resume (); else fps_mgr.pause ();

				// fps
				if (keypressedd('[')) fps_mgr.change_fps (-1);
				if (keypressedd(']')) fps_mgr.change_fps (+1);

				// vantage
				const double ds = 0.03;
				if (keydown('w')) { // move forward
					posx += ds * zx;
					posz += ds * zz;
				}
				if (keydown('s')) { // move back
					posx -= ds * zx;
					posz -= ds * zz;
				}
				if (keydown('a')) { // strafe left
					posx -= ds * xx;
					posz -= ds * xz;
				}
				if (keydown('d')) { // strafe right
					posx += ds * xx;
					posz += ds * xz;
				}
				if (keydown ('f')) posy += ds; // move up
				if (keydown('v')) posy -= ds; // move down

				if (keydown ('q')) { // zoom in
					posx += ds * zx;
					posy += ds * zy;
					posz += ds * zz;
				}
				if (keydown ('e')) { // zoom out
					posx += ds * -zx;
					posy += ds * -zy;
					posz += ds * -zz;
				}

				// calc pitch & yaw from mouse move
				const double step = 0.6;
				int dx = mousex - prev_mousex, dy = mousey - prev_mousey;
				pitch += step * dy;
				yaw += step * dx;

				// view
				if (keypressed (SDLK_RETURN)) reset_view ();

				// floor
				if (keypressed ('g')) show_floor = !show_floor;

				// particles

				if (keydown('1')) reset_sim ();
				if (keydown('2')) renew_sim ();
				if (keypressed('3')) {
					CParticle::op_all ();
					reset_sim ();
				}
				if (keypressed('4')) {
					CParticle::op_towards();
					reset_sim ();
				}
				if (keypressed ('5')) {
					CParticle::op_away ();
					reset_sim ();
				}
				if (keypressed ('6')) {
					CParticle::op_towards_away ();
					reset_sim ();
				}


				link2draw = CParticle::NOTHING;
				if (keydown ('7')) link2draw = CParticle::TOWARDS;
				if (keydown ('8')) link2draw = CParticle::AWAY;
				if (keydown ('9')) link2draw = CParticle::WATCH;

				if (keypressedd ('i')) {
					nparticles += delta_particles;
					create_particles (nparticles);
				}

				if (keypressedd ('k')) {
					nparticles = max (1, nparticles - delta_particles);
					create_particles (nparticles);
				}

				if (keypressed ('y')) {
					delta_particles *= 2;
					cons << "delta particles = " << delta_particles << eol;
				}

				if (keypressed ('h')) {
					delta_particles = max (2, delta_particles /= 2);
					cons << "delta particles = " << delta_particles << eol;
				}

				if (keypressedd ('u')) create_affects (++naffects);
				if (keypressedd ('j')) {
					naffects = max (1, naffects - 1);
					create_affects (naffects);
				}


				if (keypressed ('r')) {
					auto_reset = !auto_reset;
					cons << "auto reset ";
					if (auto_reset) {
						auto_reset_evals = fps_mgr.get_evals ();
						cons << "every " << (int) auto_reset_evals << " evals." << eol;
					} else cons << "is off." << eol;

				}
				if (auto_reset && (fps_mgr.get_evals() >= auto_reset_evals)) {
					fps_mgr.set_evals (0);
					reset_sim ();
				}

				if (keypressed ('0')) {
					// show key sim stats
					cons << "particles = " << nparticles << eol;
					cons << "ops = " << CParticle::op_print () << eol;
					cons << "affects = " << naffects << eol;
					cons << "threads = " << nthreads << eol;
					cons << "fps = " << fps_mgr.get_fps () << eol;

				}

				// console
				if (cons.is_visible()) {
					if (keypressedd (SDLK_INSERT)) --cons;
					if (keypressedd (SDLK_DELETE)) ++cons;
					if (keypressed (SDLK_HOME)) cons.home();
					if (keypressed (SDLK_END)) cons.end();
					int64_t pg0 = secs2ticks(1./2), pg1 = secs2ticks(1./16);
					if (keypressedd (SDLK_PAGEUP, pg0, pg1)) cons.pgup();
					if (keypressedd (SDLK_PAGEDOWN, pg0, pg1)) cons.pgdn();
					if (keypressed (SDLK_BACKSPACE)) cons.clear();
				}
				if (keypressed ('`')) cons.toggle ();

				return true;
			}
			return false;

		}

	void reset_view () {
		pitch = yaw = roll = 0;
		posx = POSX; posy = POSY; posz = POSZ;
	}

	void draw_cursor () {

		glPushMatrix ();
		glLoadIdentity ();
		glMatrixMode (GL_PROJECTION);
		glPushMatrix ();
		glOrtho (-1, 1, -1, 1, 0, 1);
		glCallList (cursor);
		glPopMatrix ();
		glMatrixMode (GL_MODELVIEW);
		glPopMatrix ();
	}

	void reset_sim () {
		fps_mgr.set_evals (0);
		setSeed (seed);
		CBox<double> bbox (-1., 1., 0., 2., -1., 1.);
		for (int i = 0; i < CParticle::NUM_PARTICLES; ++i) CParticle::PARTICLES[i].init (bbox);
	}

	void renew_sim () {
		seed = clock ();
		reset_sim ();
	}

	void create_particles (int np) {
		CParticle::create_particles (np);
		reset_sim ();
		setup_threads ();
		cons << "created " << np << " particles." << eol;
	}

	void create_affects (int na) {
		CParticle::create_affects (na);
		reset_sim ();
		cons << "1 particle affects " << na << " particles." << eol;
	}

	void setup_threads () {

		const int PARTICLES_PER_THREAD = 10;
		nthreads = CParticle::NUM_PARTICLES / PARTICLES_PER_THREAD;
		int dt = CParticle::NUM_PARTICLES - nthreads * PARTICLES_PER_THREAD;
		if (dt) ++nthreads;

		static int ALLOC_THREADS = 0;
		if (nthreads > ALLOC_THREADS) {
			ALLOC_THREADS = nthreads;
			destroy_threads ();
			work = new work_t [nthreads];
			threads = new pthread_t [nthreads];
		}

		int start = 0, end = 0;
		for (int i = 0; i < nthreads; ++i) {
			end += PARTICLES_PER_THREAD;
			if (end > CParticle::NUM_PARTICLES) end = CParticle::NUM_PARTICLES;
			work[i].start = start;
			work[i].end = end;
			start = end;
		}

	}

	void destroy_threads () {
		if (work) delete[] work;
		if (threads) delete[] threads;
	}

	void draw_particles () {

		for (int i = 0; i < CParticle::NUM_PARTICLES; ++i) {
			double x, y, z; CParticle::PARTICLES[i].cur_pos (x, y, z);
			double r, g, b; CParticle::PARTICLES[i].get_color(r, g, b);
			glPushMatrix();
				 glTranslated (x, y, z);
				 glColor3f (r, g, b);
				 glCallList (particle);
			glPopMatrix();
		}

	}

	void draw_links (int type) {
		for (int i = 0; i < CParticle::NUM_PARTICLES; ++i) CParticle::PARTICLES[i].draw_link (type);
	}

	void eval_particles () {

		for (int i = 0; i < nthreads; ++i) pthread_create (&threads[i], 0, worker, (void *)&work[i]);
		for (int i = 0; i < nthreads; ++i) pthread_join( threads[i], 0);
		for (int i = 0; i < CParticle::NUM_PARTICLES; ++i) CParticle::PARTICLES[i].eval_watch ();
		for (int i = 0; i < CParticle::NUM_PARTICLES; ++i) CParticle::PARTICLES[i].next ();

	}

	void setup_camera () {

		// rotate coord system for quake cam
		glRotated (roll, 0, 0, 1);
		glRotated (pitch, 1, 0, 0);
		glRotated (yaw, 0, 1, 0);

		// recompute interest and handle vectors
		double m[16]={0};
		glGetDoublev (GL_MODELVIEW_MATRIX, m);

		zx = 0; zy = 0; zz = -1;
		xx = 1; xy = 0; xz = 0;

		double tx, ty, tz;
		tx = zx * m[0] + zy * m[1] + zz * m[2] + m[3];
		ty = zx * m[4] + zy * m[5] + zz * m[6] + m[7];
		tz = zx * m[8] + zy * m[9] + zz * m[10] + m[11];
		zx = tx; zy = ty; zz = tz;

		tx = xx * m[0] + xy * m[1] + xz * m[2] + m[3];
		ty = xx * m[4] + xy * m[5] + xz * m[6] + m[7];
		tz = xx * m[8] + xy * m[9] + xz * m[10] + m[11];
		xx = tx; xy = ty; xz = tz;

		glTranslated (-posx, -posy, -posz);

	}

	public:


		project_p () : view ("p", 800, 600) {

			threads = 0;
			work = 0;

			load_help ();

			// init mouse
			read_mouse ();
			SDL_ShowCursor (0);

			//
			// init opengl
			//

			glClearColor (0,0,0,0);

			glShadeModel (GL_SMOOTH);

			// model axis
			axis = glGenLists (1);
			glNewList (axis, GL_COMPILE);
				glBegin (GL_LINES);
					 glColor3f (1, 0, 0);
					 glVertex3f (0, 0, 0);
					 glVertex3f (0, 0, -1);
					 glColor3f (0, 0, 1);
					 glVertex3f (0, 0, 0);
					 glVertex3f (0, 1, 0);
					 glColor3f (0, 1, 0);
					 glVertex3f (0, 0, 0);
					 glVertex3f (1, 0, 0);
				glEnd ();
			glEndList ();

			// model floor
			floor = glGenLists (1);
			glNewList (floor, GL_COMPILE);
			const double fc = 0.2;
			glColor3f (fc, fc, fc);
			glBegin (GL_LINES);
			for (int i = -10, j = -i, k = i; i <= 10; i++) {
					 glVertex3f (j, 0, i);
					 glVertex3f (k, 0, i);
					 glVertex3f (i, 0, j);
					 glVertex3f (i, 0, k);
			}
			glEnd();
			glEndList ();
			show_floor = true;

			// model cursor
			const double cs = 0.025;
      cursor = glGenLists (1);
			glNewList (cursor, GL_COMPILE);
			glBegin (GL_LINES);
			glColor3f (1, 1, 1);
			glVertex3f (-cs, 0, 0);
			glVertex3f (cs, 0, 0);
			glVertex3f (0, -cs, 0);
			glVertex3f (0, cs, 0);
			glEnd ();
			glEndList ();

			// model particle
      particle = glGenLists (1);
			glNewList (particle, GL_COMPILE);
				const double PARTICLE_SIZE = 0.1;
				glutSolidSphere (PARTICLE_SIZE, 16, 8);
			glEndList ();


			// setup projection ( near plane at d = -1)
			glMatrixMode (GL_PROJECTION);
			float mat[16] = {
				1, 0, 0, 0,
				0, 1, 0, 0,
				0, 0, 1, -1,
				0, 0, 0, 0
			};
			glLoadMatrixf (mat);

			// as policy set matrix mode to MODEL_VIEW
			glMatrixMode (GL_MODELVIEW);

			//
			// init p
			//

			reset_view ();
			nparticles = 256;
			delta_particles = 32;
			naffects = CParticle::NUM_AFFECTS;
			auto_reset = false;
			auto_reset_evals = 0;
			CParticle::op_all ();
			create_particles (nparticles);
		}

		~project_p () {
			SDL_ShowCursor (1);
			CParticle::destroy_particles ();
			destroy_threads ();
		}

		void operator() () {

			while (1) {

				fps_mgr.frame_starts (); 

					if (!handle_input()) break;

					if (!fps_mgr.is_paused() && fps_mgr.fps_reached ()) eval_particles();

					glClear (GL_COLOR_BUFFER_BIT);
					glLoadIdentity ();
					setup_camera ();
					if (show_floor) glCallList (floor);
					glCallList (axis);
					if (link2draw != CParticle::NOTHING) draw_links (link2draw);
					draw_particles ();
					draw_cursor ();
					if (cons.is_visible ()) cons.draw ();

					SDL_GL_SwapBuffers ();

				fps_mgr.frame_ends ();
			}

		}

};


int main (int argc, char** argv) {

  glutInit (&argc, argv);
	project_p p; p();
	return 0;
}
